package eval

import (
	"bytes"
	"fmt"
	"github.com/hashicorp/hcl/v2/gohcl"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/hashicorp/hcl/v2/hclwrite"
	"github.com/ihaiker/tfwrapper/internal/config"
	"github.com/ihaiker/tfwrapper/internal/errors"
	"os"
	"strings"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/terraform/lang"
	"github.com/ihaiker/tfwrapper/internal/hclfns"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/function"
)

type WrapperEvalContext struct {
	running *config.Running
	*hcl.EvalContext
}

func envAttr(append map[string]string) cty.Value {
	envs := make(map[string]cty.Value, 0)
	for _, name := range os.Environ() {
		kv := strings.SplitN(name, "=", 2)
		envs[kv[0]] = cty.StringVal(kv[1])
	}
	for k, v := range append {
		envs[k] = cty.StringVal(v)
	}
	return cty.ObjectVal(envs)
}

func (ec *WrapperEvalContext) fnOptions() *hclfns.FunctionOptions {
	return &hclfns.FunctionOptions{
		Stack:           ec.running.Stack,
		StackSourceDir:  ec.running.StackSourceDir,
		StackSourcesDir: ec.running.StackSourcesDir,

		ModuleName:          ec.running.ModuleName,
		ModuleDir:           ec.running.ModuleDir,
		WorkingDir:          ec.running.WorkingDir,
		TFWrapperConfigPath: ec.running.WrapperConfigPath,
		ConfigFilePath:      ec.running.CurrentConfigPath,
		TerraformVersion:    ec.running.TerraformVersion,
		TFWrapperVersion:    ec.running.TFWrapperVersion,
	}
}

func CreateEvalContext(options config.Running) (*WrapperEvalContext, hcl.Diagnostics) {
	ctx := &WrapperEvalContext{
		running: &options,
		EvalContext: &hcl.EvalContext{
			Variables: map[string]cty.Value{},
			Functions: map[string]function.Function{},
		},
	}
	ctx.Variables["env"] = envAttr(options.Environments)
	ctx.Variables["global"] = cty.ObjectVal(options.Globals)
	ctx.Variables["local"] = cty.ObjectVal(options.Locals)

	scope := lang.Scope{BaseDir: options.WorkingDir}
	for k, v := range scope.Functions() {
		ctx.EvalContext.Functions[k] = v
	}
	for k, v := range hclfns.WrapperFunctions(ctx.fnOptions()) {
		ctx.EvalContext.Functions[k] = v
	}
	for _, fn := range options.Functions {
		ctx.Functions[fn.Name] = ctx.customDefinedFunction(fn)
	}
	return ctx, nil
}

func (ec *WrapperEvalContext) Globals(globals map[string]cty.Value) *WrapperEvalContext {
	ec.running.Globals = globals
	ec.EvalContext.Variables["global"] = cty.ObjectVal(ec.running.Globals)
	return ec
}

func (ec *WrapperEvalContext) Locals(locals map[string]cty.Value) *WrapperEvalContext {
	ec.running.Locals = locals
	ec.EvalContext.Variables["local"] = cty.ObjectVal(ec.running.Locals)
	return ec
}

func (ec *WrapperEvalContext) SetVariable(name string, value cty.Value) *WrapperEvalContext {
	ec.EvalContext.Variables[name] = value
	return ec
}

func (ec *WrapperEvalContext) AppendGlobals(globals map[string]cty.Value) *WrapperEvalContext {
	if ec.running.Globals == nil {
		ec.running.Globals = map[string]cty.Value{}
	}
	for name, value := range globals {
		ec.running.Globals[name] = value
	}
	ec.EvalContext.Variables["global"] = cty.ObjectVal(ec.running.Globals)
	return ec
}

func (ec *WrapperEvalContext) AppendLocals(locals map[string]cty.Value) *WrapperEvalContext {
	if ec.running.Locals == nil {
		ec.running.Locals = map[string]cty.Value{}
	}
	for name, value := range locals {
		ec.running.Locals[name] = value
	}
	ec.EvalContext.Variables["local"] = cty.ObjectVal(ec.running.Locals)
	return ec
}

func (ec *WrapperEvalContext) DecodeBlockAttributes(block *hcl.Block) (values map[string]cty.Value, diags hcl.Diagnostics) {
	defer errors.Recover(func(cause error) {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Failed to decode block attributes",
			Detail:   cause.Error(),
		})
	})

	var attrs hcl.Attributes
	if attrs, diags = block.Body.JustAttributes(); diags.HasErrors() {
		return
	}

	values = make(map[string]cty.Value, 0)
	for name, attr := range attrs {
		if !hclsyntax.ValidIdentifier(name) {
			diags = diags.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid attribute name",
				Detail:   "A name must start with a letter and may contain only letters, digits, underscores, and dashes.",
				Subject:  &attr.NameRange,
			})
			return
		}
		value, dig := attr.Expr.Value(ec.EvalContext)
		if dig.HasErrors() {
			diags = diags.Extend(dig)
		}
		values[attr.Name] = value
	}
	return
}

func (ec *WrapperEvalContext) PartialUniqueBlock(body hcl.Body, blockName string, out interface{}) hcl.Diagnostics {
	content, _, diags := body.PartialContent(&hcl.BodySchema{
		Blocks: []hcl.BlockHeaderSchema{
			{Type: blockName},
		},
	})
	if diags.HasErrors() || len(content.Blocks) == 0 {
		return diags
	}

	if len(content.Blocks) > 1 {
		return errors.SubjectDiagnostics(
			&content.Blocks[1].DefRange,
			fmt.Sprintf("%s block must be unique", blockName),
			fmt.Errorf("%s block must be unique, but found %d in file %s",
				blockName, len(content.Blocks), ec.running.CurrentConfigPath),
		)
	}

	defer errors.Recover(func(cause error) {
		diags = diags.Append(&hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "Failed to decode block",
			Detail:   fmt.Sprintf("Decode block %s: %#v", content.Blocks[0].Type, cause),
		})
	})
	return gohcl.DecodeBody(content.Blocks[0].Body, ec.EvalContext, out)
}

func (ec *WrapperEvalContext) RenderHclBody(body hcl.Body) (str string, diags hcl.Diagnostics) {
	if _, match := body.(*hclsyntax.Body); !match {
		return "", errors.NewDiagnostics("Invalid type", "only support hclsyntax.Body")
	}
	buf := bytes.NewBufferString("")

	f := hclwrite.NewEmptyFile()
	if diags = writeHclSyntaxTo(f.Body(), ec.EvalContext, body.(*hclsyntax.Body)); diags.HasErrors() {
		return
	}
	if _, err := f.WriteTo(buf); err != nil {
		diags = diags.Extend(errors.Diagnostics("writer file", err))
	} else {
		str = string(hclwrite.Format(buf.Bytes()))
	}
	return
}

func writeHclSyntaxBodyAttr(body *hclwrite.Body, block *hclsyntax.Body, ctx *hcl.EvalContext) hcl.Diagnostics {
	for name, attribute := range block.Attributes {
		if value, diags := attribute.Expr.Value(ctx); diags.HasErrors() {
			return diags
		} else {
			body.SetAttributeValue(name, value)
		}
	}
	return nil
}

func writeHclSyntaxTo(dest *hclwrite.Body, ctx *hcl.EvalContext, from *hclsyntax.Body) hcl.Diagnostics {
	if diags := writeHclSyntaxBodyAttr(dest, from, ctx); diags.HasErrors() {
		return diags
	}
	for _, block := range from.Blocks {
		newBlock := dest.AppendNewBlock(block.Type, block.Labels)
		if diags := writeHclSyntaxTo(newBlock.Body(), ctx, block.Body); diags.HasErrors() {
			return diags
		}
	}
	return nil
}
